package NEHE.JOGL.Port;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.Random;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.swing.*;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureCoords;
import com.sun.opengl.util.texture.TextureIO;
import static javax.media.opengl.GL.*;
import static java.awt.event.KeyEvent.*;

/**
 * NeHe Lesson 19a: Firework
 */
public class NeheJOGL19Firework extends JPanel implements GLEventListener, KeyListener {
   private static final int REFRESH_FPS = 60;    // Display refresh frames per second
   private GLU glu;             // For the GL Utility
   final FPSAnimator animator;  // Used to drive display() 
   GLCanvas canvas;
   
   private static final int MAX_PARTICLES = 1000; // max number of particles
   private Particle[] particles = new Particle[MAX_PARTICLES];

   private static boolean enabledBurst = false;
   
   // Pull forces in each direction
   private static float gravityY = -0.0008f; // gravity

   // Global speed for all the particles
   private static float speedXGlobal = 0.0f;
   private static float speedYGlobal = 0.1f;
   private static float z = -40.0f;
   private static float y = 5.0f;

   // Texture applied over the shape
   private Texture texture;
   private String textureFileName = "images/star.bmp";
   private String textureFileType = ".bmp";

   // Texture image flips vertically. Shall use TextureCoords class to retrieve the
   // top, bottom, left and right coordinates.
   private float textureTop;
   private float textureBottom;
   private float textureLeft;
   private float textureRight;

   // Constructor
   public NeheJOGL19Firework() {
      canvas = new GLCanvas();
      this.setLayout(new BorderLayout());
      this.add(canvas, BorderLayout.CENTER);
      canvas.addGLEventListener(this);
      canvas.addKeyListener(this);
      canvas.setFocusable(true);  // To receive key event
      canvas.requestFocus();
   
      // Run the animation loop using the fixed-rate Frame-per-second animator,
      // which calls back display() at this fixed-rate (FPS).
      animator = new FPSAnimator(canvas, REFRESH_FPS, true);
   }

   // Main program
   public static void main(String[] args) {
      final int WINDOW_WIDTH = 320;
      final int WINDOW_HEIGHT = 240;
      final String WINDOW_TITLE = "Nehe #19a: Firework";

      JFrame frame = new JFrame();
      final NeheJOGL19Firework joglMain = new NeheJOGL19Firework();
      frame.setContentPane(joglMain);
      frame.addWindowListener(new WindowAdapter() {
         @Override 
         public void windowClosing(WindowEvent e) {
            // Use a dedicate thread to run the stop() to ensure that the
            // animator stops before program exits.
            new Thread() {
               @Override 
               public void run() {
                  joglMain.animator.stop(); // stop the animator loop
                  System.exit(0);
               }
            }.start();
         }
      });
      frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
      frame.setTitle(WINDOW_TITLE);
      frame.setVisible(true);
      joglMain.animator.start(); // start the animation loop
   }

   // ------ Implement methods declared in GLEventListener ------

   /**
    * Called by the drawable immediately after the OpenGL context is initialized for
    * the first time. Can be used to perform one-time OpenGL initialization such as
    * setup of lights and display lists. Run only once.
    */
   @Override
   public void init(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // GL Utilities
      glu = new GLU();
      // Enable smooth shading, which blends colors nicely across a polygon, and
      // smoothes out lighting.
      gl.glShadeModel(GL_SMOOTH);
      // Set background color (in RGBA). Alpha of 0 for total transparency
      gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
      // Setup the depth buffer & enable the depth testing
      gl.glClearDepth(1.0f);
      //gl.glEnable(GL_DEPTH_TEST); // enables depth testing
      //gl.glDepthFunc(GL_LEQUAL); // the type of depth test to do
      // We want the best perspective correction to be done
      gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

      gl.glEnable(GL_BLEND); // Enable Blending
      gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Type Of Blending To Perform
      gl.glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); // Really Nice Point Smoothing

      // Load the texture image
      try {
         // Create a OpenGL Texture object from (URL, mipmap, file suffix)
         // Use URL so that can read from JAR and disk file.
         texture = TextureIO.newTexture(
               this.getClass().getResource(textureFileName), false, textureFileType);
      } catch (GLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      // Use linear filter for texture if image is larger than the original texture
      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      // Use linear filter for texture if image is smaller than the original texture
      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

      // Texture image flips vertically. Shall use TextureCoords class to retrieve
      // the top, bottom, left and right coordinates, instead of using 0.0f and 1.0f.
      TextureCoords textureCoords = texture.getImageTexCoords();
      textureTop = textureCoords.top();
      textureBottom = textureCoords.bottom();
      textureLeft = textureCoords.left();
      textureRight = textureCoords.right();

      // Initialize the particles
      for (int i = 0; i < MAX_PARTICLES; i++) {
         particles[i] = new Particle();
      }
   }

   /**
    * Called by the drawable to initiate OpenGL rendering.
    */
   @Override
   public void display(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // Clear the screen and the depth buffer
      gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      // Reset the view (x, y, z axes back to normal)
      gl.glLoadIdentity();

      // Render the particles
      for (int i = 0; i < MAX_PARTICLES; i++) {
         if (particles[i].active) {
            // Draw The Particle Using Our RGB Values, Fade The Particle Based On
            // It's Life
            gl.glColor4f(particles[i].r, particles[i].g, particles[i].b,
                  particles[i].life);

            texture.enable();
            texture.bind();

            gl.glBegin(GL_TRIANGLE_STRIP); // Build Quad From A Triangle Strip

            float x = particles[i].x;
            float y = particles[i].y + this.y;
            float z = particles[i].z + this.z;

            gl.glTexCoord2d(textureRight, textureTop);
            gl.glVertex3f(x + 0.5f, y + 0.5f, z); // Top Right
            gl.glTexCoord2d(textureLeft, textureTop);
            gl.glVertex3f(x - 0.5f, y + 0.5f, z); // Top Left
            gl.glTexCoord2d(textureRight, textureBottom);
            gl.glVertex3f(x + 0.5f, y - 0.5f, z); // Bottom Right
            gl.glTexCoord2d(textureLeft, textureBottom);
            gl.glVertex3f(x - 0.5f, y - 0.5f, z); // Bottom Left
            gl.glEnd();

            // Move the particle
            particles[i].x += particles[i].speedX;
            particles[i].y += particles[i].speedY;
            particles[i].z += particles[i].speedZ;

            // Apply the gravity force on y-axis
            particles[i].speedY += gravityY;
            
            if (enabledBurst) {
               particles[i].burst();
            }
         }
      }
      if (enabledBurst) enabledBurst = false;
   }

   /**
    * Called by the drawable during the first repaint after the component has been
    * resized. Run at least once when the window is first set to visible.
    */
   @Override
   public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();

      if (height == 0) {
         height = 1; // prevent divide by zero
      }
      float aspect = (float)width / height;

      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);

      // Enable perspective view (where far objects are smaller)
      gl.glMatrixMode(GL_PROJECTION);
      gl.glLoadIdentity(); // reset
      glu.gluPerspective(45.0f, aspect, 0.1f, 200.0f); // fovy, aspect, zNear, zFar

      // Enable the model view - any new transformations will affect the model-view
      // matrix
      gl.glMatrixMode(GL_MODELVIEW);
      gl.glLoadIdentity(); // reset
   }

   /**
    * Called when the display mode (eg. resolution) has been changed.
    */
   @Override
   public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
         boolean deviceChanged) {}

   class Particle {
      boolean active; // always active in this program
      float life; // life time
      float fade; // fading speed, which reduces the life time
      float r, g, b; // color
      float x, y, z; // position
      float speedX, speedY, speedZ; // speed in the direction

      private final float[][] colors = // Rainbow Of 12 Colors
      { { 1.0f, 0.5f, 0.5f }, { 1.0f, 0.75f, 0.5f }, { 1.0f, 1.0f, 0.5f },
            { 0.75f, 1.0f, 0.5f }, { 0.5f, 1.0f, 0.5f }, { 0.5f, 1.0f, 0.75f },
            { 0.5f, 1.0f, 1.0f }, { 0.5f, 0.75f, 1.0f }, { 0.5f, 0.5f, 1.0f },
            { 0.75f, 0.5f, 1.0f }, { 1.0f, 0.5f, 1.0f }, { 1.0f, 0.5f, 0.75f } };

      private Random rand = new Random();

      // Constructor
      public Particle() {
         active = true;
         burst();
      }

      public void burst() {
         life = 1.0f;

         // Set a random fade speed value between 0.003 and 0.103
         fade = rand.nextInt(100) / 1000.0f + 0.003f;

         // Set the initial position
         x = y = z = 0.0f;
         
         // Generate a random speed and direction in polar coordinate, then resolve
         // them into x and y.
         float maxSpeed = 0.1f;
         float speed = 0.02f + (rand.nextFloat() - 0.5f) * maxSpeed; 
         float angle = (float)Math.toRadians(rand.nextInt(360));

         speedX = speed * (float)Math.cos(angle);
         speedY = speed * (float)Math.sin(angle) + speedYGlobal;
         speedZ = (rand.nextFloat() - 0.5f) * maxSpeed;

         int colorIndex = (int)(((speed - 0.02f) + maxSpeed) / (maxSpeed * 2) * colors.length) % colors.length;
         // Pick a random color
         r = colors[colorIndex][0];
         g = colors[colorIndex][1];
         b = colors[colorIndex][2];
      }
   }

   @Override
   public void keyPressed(KeyEvent e) {
      switch (e.getKeyCode()) {
         case VK_T:
            if (!enabledBurst) enabledBurst = true;
            break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {}

   @Override
   public void keyTyped(KeyEvent e) {}
}